/**
 * Copyright (C) 2010, 2011 Neofonie GmbH
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package eu.dicodeproject.blogimport.filereader;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.zip.GZIPInputStream;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.tools.tar.TarEntry;
import org.apache.tools.tar.TarInputStream;

import eu.dicodeproject.blogimport.exceptions.FileAccessException;

/**
 * This implementation of an {@link FileSource} scans the initialy referenced
 * directory for tar.gz files and retrieves the xml files contained in these
 * tar.gz files with every call of hasNext and next. It is mandatory to call
 * hasNext because this method handles the access to the tar.gz files and seeks
 * for the next xml file thats content shall be return by the next invocation of
 * the next method.
 * 
 * 
 * 
 */
public class GZipXMLFileSource implements FileSource {

	private static final FileSuffixFilter FILTER = new FileSuffixFilter(".gz");

	private static final Logger LOG = LoggerFactory.getLogger(GZipXMLFileSource.class);

	private String gzDir;

	private List<File> gzFileList;

	private TarEntry actTar;

	private TarInputStream in;

	private File actFile;

	/**
	 * C'tor that sets the scanning directory which contains the content this
	 * {@link FileSource} should return.
	 * 
	 * @param dir
	 *            the directory that holds all tar.gz files
	 */
	public GZipXMLFileSource(final String dir) {
		if (dir == null) {
			throw new IllegalArgumentException("No path referenced.");
		}
		this.gzDir = dir;
	}

	/**
	 * The implementation of this method scans the directory for tar.gz files.
	 * 
	 * @return true, if successful
	 * @see eu.dicodeproject.blogimport.filereader.FileSource#refresh()
	 */
	@Override
	public boolean init() {

		File directory = new File(this.gzDir);
		if (directory == null || !directory.exists() || !directory.isDirectory()) {
			throw new IllegalArgumentException("The directory " + directory + " is invalid.");
		}

		this.gzFileList = new ArrayList<File>();
		Collections.addAll(this.gzFileList, directory.listFiles(FILTER));
		if (LOG.isInfoEnabled()) {
			LOG.info("Found " + this.gzFileList + " files, ready to access them.");
		}
		return this.gzFileList.size() > 0;
	}

	/**
	 * This implementation moves the cursor inside the tar.gz streams to the
	 * next xml file and marks this position for a possible content retrieval by
	 * calling the next() method.
	 * 
	 * @return true, if a xml file was found inside a tar.gz file
	 * @see eu.dicodeproject.blogimport.filereader.FileSource#hasNext()
	 */
	@Override
	public boolean hasNext() {
		// when we do not have an open stream to an gz file and the list of gz
		// files is empty, we can return false here
		if (this.in == null && (this.gzFileList == null || this.gzFileList.size() == 0)) {
			return false;
		}
		// we do just have an valid entry, so we do not need to look up the next
		// one
		if (this.actTar != null) {
			return true;
		}

		// when we made it here we need a new xml entry from the current or next
		// gz file
		boolean nextEntryFound = false;
		boolean exhausted = false;

		// cycle as long you do find something or you run out of gz files
		while (!nextEntryFound && !exhausted) {
			// the stream is null, what means that we have no open gz file so
			// lets open a stream to the next tar.gz file
			if (in == null) {
				LOG.info("Trying to open new stream.");
				try {
					this.actFile = this.gzFileList.remove(0);
					LOG.info("New archive file is " + this.actFile.getAbsolutePath() + ".");
					this.in = new TarInputStream(new GZIPInputStream(new FileInputStream(this.actFile)));
				} catch (FileNotFoundException e) {
					throw new FileAccessException("Archive file " + this.actFile + " does not exist.", e);
				} catch (IOException e) {
					throw new FileAccessException("Error when accessing " + this.actFile + " as tar.gz stream.", e);
				}
			}
			// now we cycle through the content of the gz file until we find the
			// next valid xml file
			try {
				// IMPORTANT: lets move the input streams inner cursor to the
				// next offset that holds
				// the content of a xml file with the help of actTar
				while (!nextEntryFound && (this.actTar = this.in.getNextEntry()) != null) {
					if (!this.actTar.isDirectory() && this.actTar.getName().endsWith(".xml")) {
						LOG.debug("Next entry: " + this.actTar.getName());
						nextEntryFound = true;
					}
				}
				// we did not find a new xml file, so we are closing the stream
				// and wait for the next gz file from the outer while loop
				if (this.actTar == null) {
					try {
						LOG.info("Current archive finished, closing stream.");
						this.in.close();
					} catch (IOException e) {
						throw new FileAccessException("Error when closing current archive stream.", e);
					}
					this.in = null;
				}
			} catch (IOException e1) {
				this.actTar = null;
				this.in = null;
				throw new FileAccessException("Error when traversing archive content.", e1);
			}
			// exhausted means that we've run out of gz files, so we will need
			// to wait until the user calls refresh again
			exhausted = (this.in == null && this.gzFileList.size() == 0);
		}
		return this.actTar != null;
	}

	/**
	 * @see eu.dicodeproject.blogimport.filereader.FileSource#next()
	 */
	@Override
	public String next() {
		// ok, when actTar is null here this can have two possibilities (a. the
		// user did not call hasNext() before; b. there is no xml inside
		// whatever gz file and the user ignored the false return value of
		// hasNext())
		if (this.actTar == null) {
			return null;
		}

		// now lets copy the content of the in stream to an byte array, that
		// will be put into an StreamSource
		// IMPORTANT: before there might be some misleading interpretation: we
		// do not use
		// actTar here because it does not hold the content, the interaction
		// within the hasNext() method just moves the InputStream cursor to the
		// correct spot in the stream and for a correct synchronization it is
		// absolutly necessary that actTar will be set to null after we got the
		// content from the stream so that hasNext() will work properly
		ByteArrayOutputStream out = new ByteArrayOutputStream();
		try {
			if (this.in.getRecordSize() > 0) {
				this.in.copyEntryContents(out);
			}
		} catch (IOException e) {
			this.actTar = null;
			this.in = null;
			try {
				boolean created = new File(this.actFile.getAbsolutePath() + ".fail").createNewFile();
				LOG.debug("Statefile created? " + created);
			} catch (IOException e1) {
				LOG.error("Cannot create failure state file.");
			}
			throw new FileAccessException("Error while retrieving the content of the xml file from the input stream.",
					e);
		}
		byte[] tmp = out.toByteArray();
		String cntnt = new String(tmp);
		if (!cntnt.startsWith("<")) {
			cntnt = "<" + cntnt;
		}
		this.actTar = null;
		return cntnt;
	}

	/**
	 * @see eu.dicodeproject.blogimport.filereader.FileSource#shutdown()
	 */
	@Override
	public void shutdown() {
		// not implemented
	}
}
